﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using CNUtility;
using CNShapes;
using CNStreaming;

namespace CNNetworking
{
    /// <summary>
    /// This class handles the server-side of the networking communication. It
    /// implements the IClientToServer interface, and the methods on this
    /// interface will be called when we receive messages from the clients.
    /// 
    /// The IClientToServer interface has an associated callback-interface: 
    /// IServerToClient. In the connect() method, we store the callback object
    /// for each client, and we can call back to them via this interface.
    /// 
    /// This class uses WCF to do the communication.
    /// 
    /// This class is a singleton.
    /// </summary>
    [ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Single, InstanceContextMode = InstanceContextMode.Single)]
    public class NetworkingServer : IClientToServer
    {
        #region Public methods

        /// <summary>
        /// Singleton's getInstance method.
        /// </summary>
        public static NetworkingServer getInstance()
        {
            if (m_Instance == null)
            {
                m_Instance = new NetworkingServer();
            }
            return m_Instance;
        }

        /// <summary>
        /// Starts listening for client connections.
        /// </summary>
        public void startServer()
        {
            // We create the listen address...
            int iPort = UserPreferences.getPropertyBag().getInt("Networking.Port");
            string strAddress = "net.tcp://localhost:" + iPort.ToString() + "/Whiteboard/";
            Uri baseAddress = new Uri(strAddress);

            // And open the conenction...
            m_Host = new ServiceHost(this, baseAddress);
            m_Host.Open();
        }

        /// <summary>
        /// We stop listening for client connections.
        /// </summary>
        public void stopServer()
        {
            if (m_Host == null)
            {
                return;
            }

            // We notify our clients that we are disconnecting...
            foreach (ClientInfo ci in m_ClientInfos)
            {
                ci.ClientCallback.onDisconnected();
            }

            m_Host.Close();
            m_Host = null;
        }

        #endregion

        #region IClientToServer members

        /// <summary>
        /// Called when a new client connects to the server.
        /// </summary>
        public void connect(Guid clientID, string strName)
        {
            // We store the client info for this connection...
            ClientInfo clientInfo = new ClientInfo();
            clientInfo.ClientCallback = OperationContext.Current.GetCallbackChannel<IServerToClient>();
            clientInfo.ClientID = clientID;
            clientInfo.Name = strName;
            clientInfo.IsServer = clientID.Equals(NetworkingClient.getInstance().ClientID);
            m_ClientInfos.Add(clientInfo);

            // We publish the new client-list...
            publishClientList();

            // We ask the client about any other clients it knows about (and is 
            // willing to share with us), so that we can share this information 
            // with other clients...
            clientInfo.ClientCallback.requestAddressBook();

            // We synchronize the client's session with our one (provided we are not the server)...
            if (clientInfo.IsServer == false)
            {
                sendSessionToClient(clientInfo);
            }
        }

        /// <summary>
        /// Called when we receive a disconnect message from a client.
        /// </summary>
        public void disconnect(Guid clientID)
        {
            // We remove the client from our list, and notify remaining clients
            // that the client-list has changed...
            foreach (ClientInfo ci in m_ClientInfos)
            {
                if (ci.ClientID.Equals(clientID))
                {
                    m_ClientInfos.Remove(ci);
                    break;
                }
            }

            publishClientList();
        }

        /// <summary>
        /// Called by a client with its server connection list. We share these with
        /// all other connected clients.
        /// </summary>
        public void sendAddressBook(IList<ConnectionSettings> serverConnectionSettings)
        {
            foreach (ClientInfo ci in m_ClientInfos)
            {
				ci.ClientCallback.onNewAddressBook(serverConnectionSettings);
            }
        }

        /// <summary>
        /// Called by the client with an update to a shape for a particular session
        /// and page in that session.
        /// </summary>
        public void shapeEdited(Guid clientID, Guid sessionID, Guid pageID, byte[] shapeData)
        {
            // We send the edited shape to all clients (except the one that sent it to
            // us)...
            foreach (ClientInfo ci in m_ClientInfos)
            {
				if (ci.ClientID == clientID) continue;
                ci.ClientCallback.onUpdatedShape(sessionID, pageID, shapeData, clientID);
            }
        }

        /// <summary>
        /// Called by the client to remove a shape from a page. 
        /// </summary>
        public void shapeRemoved(Guid clientID, Guid sessionID, Guid pageID, Guid shapeID)
        {
            // We forward the shape-removal to all clients except the one that
            // sent it...
            foreach (ClientInfo ci in m_ClientInfos)
            {
				if (ci.ClientID == clientID) continue;
                ci.ClientCallback.onRemovedShape(sessionID, pageID, shapeID, clientID);
            }
        }

		/// <summary>
		/// Called when a client opens a new session.
		/// </summary>
		public void newSession(Guid clientID, Guid sessionID)
		{
			// We notify all clients (except the sender) about the new session...
			foreach (ClientInfo ci in m_ClientInfos)
			{
				if (ci.ClientID == clientID) continue;
				ci.ClientCallback.onNewSession(sessionID, clientID);
			}
		}

		/// <summary>
		/// Called when a client opens a new page.
		/// </summary>
		public void newPage(Guid clientID, Guid sessionID, Guid pageID)
		{
			// We notify all clients (except the sender) about the new page...
			foreach (ClientInfo ci in m_ClientInfos)
			{
				if (ci.ClientID == clientID) continue;
				ci.ClientCallback.onNewPage(sessionID, pageID, clientID);
			}
		}

		/// <summary>
		/// Called when a client changes the active page.
		/// </summary>
		public void activePageChanged(Guid clientID, Guid sessionID, Guid pageID)
		{
			// We notify all clients (except the sender) about the new page...
			foreach (ClientInfo ci in m_ClientInfos)
			{
				if (ci.ClientID == clientID) continue;
				ci.ClientCallback.onActivePageChanged(sessionID, pageID, clientID);
			}
		}

        #endregion

        #region Private functions

        /// <summary>
        /// Private constructor (for singleton).
        /// </summary>
        private NetworkingServer()
        {
        }

        /// <summary>
        /// Publishes the list of currently connected clients to all the connected
        /// clients, so that they know who is part of the current session.
        /// </summary>
        private void publishClientList()
        {
            // We create a list of string names...
            IList<string> names = new List<string>();
            foreach (ClientInfo ci in m_ClientInfos)
            {
                names.Add(ci.Name);
            }

            // We publish the names to all the clients...
            foreach (ClientInfo ci in m_ClientInfos)
            {
                try { ci.ClientCallback.onClientListUpdated(names); }
                catch (Exception) { } // We silently catch any networking exceptions. 
            }
        }

        /// <summary>
        /// Returns the networking client ID for this app.
        /// </summary>
        private Guid getClientID()
        {
            return NetworkingClient.getInstance().ClientID;
        }

        /// <summary>
        /// We send the session info to the client. This includes all pages
        /// in the session.
        /// </summary>
        private void sendSessionToClient(ClientInfo clientInfo)
        {
            Session session = Session.getInstance();
            IServerToClient callback = clientInfo.ClientCallback;

            // We tell the client to start a new session...
            callback.onNewSession(session.ID, getClientID());

            // We add the pages to the client session...
            foreach (Page page in session.Pages)
            {
                // We tell the client about the page...
                callback.onNewPage(session.ID, page.ID, getClientID());

                // We add the shapes for this page...
                foreach (Shape shape in page.Shapes)
                {
                    byte[] shapeData = StreamHelper.convertToByteArray(shape);
                    callback.onNewShape(session.ID, page.ID, shapeData, getClientID());
                }
            }

            // We set the active page...
            if (session.getActivePage() != null)
            {
                callback.onActivePageChanged(session.ID, session.getActivePage().ID, getClientID());
            }
        }

        #endregion

        #region Private data

        /// <summary>
        /// Holds information about one client that we are connected to.
        /// </summary>
        private class ClientInfo
        {
            public Guid ClientID { get; set; }
            public string Name { get; set; }
            public IServerToClient ClientCallback { get; set; }
            public bool IsServer { get; set; }
        }

        // Singleton instance...
        private static NetworkingServer m_Instance = null;

        private ServiceHost m_Host = null;
        private IList<ClientInfo> m_ClientInfos = new List<ClientInfo>();

        #endregion

    }
}
